Home

Using Generics to Decode JSON in Swift

Generics are one of the most powerful features of Swift, and much of the Swift standard library is built with generic code. By using generics, you can write flexible and reusable functions and types that help you avoid duplication and make your code more readable.

Generic Functions

Generic functions can work with any type. Here is a non-generic function that adds two integer values together and then returns the result:

func add(_ firstValue: Int, _ secondValue: Int) -> Int {
     firstValue + secondValue
}

But what if we want to have a function that performs this kind of calculation on any type of values, rather than just integers? Here is a generic function:

func add<T: Numeric>(_ firstValue: T, _ secondValue: T) -> T {
     firstValue + secondValue
}
// <T>: is a placeholder type inside angle brackets.
// Numeric: a type constraint that requires T to conform to the Numeric protocol.

In the two examples below, T is inferred to be Int and Double, respectively:

let intCalculation = add(2,3) // 5
let doubleCalculation = add(5.2, 3.6) // 8.8

Using Generic Functions to Decode JSON

Let's say we have two different JSON files. One file contains chemistry class information, and the other file contains details of a student in that class.

// Chemistry-Class-info.json
{
    "class": "chemistry",
    "teacher": "Heisenberg",
    "students": [
       {
           "id": 1,
           "first_name": "Jesse",
           "last_name": "Pinkman"
        },
        {
           "id": 2,
           "first_name": "Saul",
           "last_name": "Goodman"
        },
        {
            "id": 3,
           "first_name": "Mike",
           "last_name": "Ehrmantraut"
        }
    ],
    "place": "RV"
}

// Student-details.json
{
   "id": 1,
   "first_name": "Jesse",
   "last_name": "Pinkman"
}

To parse the JSON data, we need to define our structs, which are referred to as models.

// MARK: - Class
struct Class: Codable {
    let className: String 
    let teacher: String
    let students: [Student]
    let place: String
}

// MARK: - Student
struct Student: Codable {
    let id: Int
    let firstName: String
    let lastName: String
}

Our structs both conform to Codable, which is a protocol that allows us to decode JSON into our Class, and Student model types. Here's how you can write a generic function to handle this:

struct DataManager {

   static func decode<T: Codable>(file: String, type: T.Type) -> T? {
      guard let path = Bundle.main.path(forResource: file, type: "json") else {
         fatalError("Failed to find this \(file).")
     }
     guard let data = FileManager.default.contents(atPath: path) else {
        fatalError("Failed to load this \(file).")
     }
    
     let decoder = JSONDecoder()
     decoder.keyDecodingStrategy = .convertFromSnakeCase
     do {
        return try decoder.decode(T.self, from: data)
     } catch {
        print(error)
        return nil
     }
   }
}

Now we can use this function to decode these JSON files to the types that we pass in (in this case, Class and Student). Note: Make sure you don't misspell the file names.

var chemistryClass: Class?
var student: Student?

chemistryClass = try? DataManager.decode(file: "Chemistry-Class-info", type: Class.self)
if let chemistryClass = chemistryClass {
   print("Teacher: \(chemistryClass.teacher)") // Teacher: Heisenberg
   print("Place: \(chemistryClass.place)")     // Place: RV
}

student = try? DataManager.decode(file: "Student-details", type: Student.self)
if let student = student {
   print("Student: \(student.firstName)")  // Student: Jesse
}

That's it! Now you know how to decode JSON using generics in Swift. This approach not only makes your code more reusable and easier to maintain but also leverages the power of Swift's type system to ensure type safety and clarity.